/*
 * Copyright GoInstant, Inc. and other contributors. All rights reserved.
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */
var cookieJar = {};
(function () {


    'use strict';

    var getPublicSuffix = function getPublicSuffix(domain) {
        return window.publicSuffixList.getPublicSuffix(domain);
    };

    cookieJar.getPublicSuffix = getPublicSuffix;

    var urlParse = function (url) {
        var parser = document.createElement('a');
        parser.href = url;
        return parser;
    };

    function MemoryCookieStore(firebaseEndpoint) {
        this.idx = {};
        this.firebaseEndpoint = firebaseEndpoint;
    }

    var MemoryCookieStore = MemoryCookieStore;
    MemoryCookieStore.prototype.idx = null;
    MemoryCookieStore.prototype.synchronous = true;

// force a default depth:
    MemoryCookieStore.prototype.inspect = function () {
        return "{ idx: " + util.inspect(this.idx, false, 2) + ' }';
    };

    MemoryCookieStore.prototype.findCookie = function (domain, path, key, cb) {
        if (!this.idx[domain]) {
            return cb(null, undefined);
        }
        if (!this.idx[domain][path]) {
            return cb(null, undefined);
        }
        return cb(null, this.idx[domain][path][key] || null);
    };

    MemoryCookieStore.prototype.findCookies = function (domain, path) {
        var results = [];
        if (!domain) {
            return [];
        }

        var pathMatcher;
        if (!path) {
            // null or '/' means "all paths"
            pathMatcher = function matchAll(domainIndex) {
                for (var curPath in domainIndex) {
                    var pathIndex = domainIndex[curPath];
                    for (var key in pathIndex) {
                        results.push(pathIndex[key]);
                    }
                }
            };

        } else if (path === '/') {
            pathMatcher = function matchSlash(domainIndex) {
                var pathIndex = domainIndex['/'];
                if (!pathIndex) {
                    return;
                }
                for (var key in pathIndex) {
                    results.push(pathIndex[key]);
                }
            };

        } else {
            var paths = permutePath(path) || [path];
            pathMatcher = function matchRFC(domainIndex) {
                paths.forEach(function (curPath) {
                    var pathIndex = domainIndex[curPath];
                    if (!pathIndex) {
                        return;
                    }
                    for (var key in pathIndex) {
                        results.push(pathIndex[key]);
                    }
                });
            };
        }

        var domains = permuteDomain(domain) || [domain];
        var idx = this.idx;
        domains.forEach(function (curDomain) {
            var domainIndex = idx[curDomain];
            if (!domainIndex) {
                return;
            }
            pathMatcher(domainIndex);
        });

        return results;
    };

    MemoryCookieStore.prototype.putCookie = function (cookie, cb) {
        if (!this.idx[cookie.domain]) {
            this.idx[cookie.domain] = {};
        }
        if (!this.idx[cookie.domain][cookie.path]) {
            this.idx[cookie.domain][cookie.path] = {};
        }
        this.idx[cookie.domain][cookie.path][cookie.key] = cookie;
        var cookieKey = btoa(cookie.domain + ';' + cookie.path + ";" + cookie.key);
        /*this.firebaseEndpoint.child(cookieKey)
            .set({setCookie: cookie.toString(), domain: cookie.domain, path: cookie.path, keepAliveUrl: firstAccessUrls[cookieKey] ? firstAccessUrls[cookieKey] : null});*/
        cb(null);
    };

    MemoryCookieStore.prototype.updateCookie = function updateCookie(oldCookie, newCookie, cb) {
        // updateCookie() may avoid updating cookies that are identical.  For example,
        // lastAccessed may not be important to some stores and an equalitygetPublicSuffix
        // comparison could exclude that field.
        this.putCookie(newCookie, cb);
    };

    MemoryCookieStore.prototype.removeCookie = function removeCookie(domain, path, key, cb) {
        if (this.idx[domain] && this.idx[domain][path] && this.idx[domain][path][key]) {
            delete this.idx[domain][path][key];
        }
        //this.firebaseEndpoint.child(btoa(domain + ';' + path + ";" + key)).remove();
        cb(null);
    };

    MemoryCookieStore.prototype.removeCookies = function removeCookies(domain, path, cb) {
        if (this.idx[domain]) {
            if (path) {
                delete this.idx[domain][path];
            } else {
                delete this.idx[domain];
            }
        }
        return cb(null);
    };


    var DATE_DELIM = /[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]/;

// From RFC2616 S2.2:
    var TOKEN = /[\x21\x23-\x26\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7A\x7C\x7E]/;

// From RFC6265 S4.1.1
// note that it excludes \x3B ";"
    var COOKIE_OCTET = /[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/;
    var COOKIE_OCTETS = new RegExp('^' + COOKIE_OCTET.source + '$');

// The name/key cannot be empty but the value can (S5.2):
    var COOKIE_PAIR_STRICT = new RegExp('^(' + TOKEN.source + '+)=("?)(' + COOKIE_OCTET.source + '*)\\2$');
    var COOKIE_PAIR = /^([^=\s]+)\s*=\s*\s*(.*)\s*$/;

// RFC6265 S4.1.1 defines extension-av as 'any CHAR except CTLs or ";"'
// Note ';' is \x3B
    var NON_CTL_SEMICOLON = /[\x20-\x3A\x3C-\x7E]+/;
    var EXTENSION_AV = NON_CTL_SEMICOLON;
    var PATH_VALUE = NON_CTL_SEMICOLON;

// Used for checking whether or not there is a trailing semi-colon
    var TRAILING_SEMICOLON = /;+$/;

    /* RFC6265 S5.1.1.5:
     * [fail if] the day-of-month-value is less than 1 or greater than 31
     */
    var DAY_OF_MONTH = /^(0?[1-9]|[12][0-9]|3[01])$/;

    /* RFC6265 S5.1.1.5:
     * [fail if]
     * *  the hour-value is greater than 23,
     * *  the minute-value is greater than 59, or
     * *  the second-value is greater than 59.
     */
    var TIME = /(0?[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])/;
    var STRICT_TIME = /^(0?[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/;

    var MONTH = /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)$/i;
    var MONTH_TO_NUM = {
        jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5,
        jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11
    };
    var NUM_TO_MONTH = [
        'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
    ];
    var NUM_TO_DAY = [
        'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'
    ];

    var YEAR = /^([1-9][0-9]{1,3})$/; // 2 to 4 digits

    var MAX_TIME = 2147483647000; // 31-bit max
    var MIN_TIME = 0; // 31-bit min


// RFC6265 S5.1.1 date parser:
    function parseDate(str, strict) {
        if (!str) {
            return;
        }
        var found_time, found_dom, found_month, found_year;

        /* RFC6265 S5.1.1:
         * 2. Process each date-token sequentially in the order the date-tokens
         * appear in the cookie-date
         */
        var tokens = str.split(DATE_DELIM);
        if (!tokens) {
            return;
        }

        var date = new Date();
        date.setMilliseconds(0);

        for (var i = 0; i < tokens.length; i++) {
            var token = tokens[i].trim();
            if (!token.length) {
                continue;
            }

            var result;

            /* 2.1. If the found-time flag is not set and the token matches the time
             * production, set the found-time flag and set the hour- value,
             * minute-value, and second-value to the numbers denoted by the digits in
             * the date-token, respectively.  Skip the remaining sub-steps and continue
             * to the next date-token.
             */
            if (!found_time) {
                result = (strict ? STRICT_TIME : TIME).exec(token);
                if (result) {
                    found_time = true;
                    date.setUTCHours(result[1]);
                    date.setUTCMinutes(result[2]);
                    date.setUTCSeconds(result[3]);
                    continue;
                }
            }

            /* 2.2. If the found-day-of-month flag is not set and the date-token matches
             * the day-of-month production, set the found-day-of- month flag and set
             * the day-of-month-value to the number denoted by the date-token.  Skip
             * the remaining sub-steps and continue to the next date-token.
             */
            if (!found_dom) {
                result = DAY_OF_MONTH.exec(token);
                if (result) {
                    found_dom = true;
                    date.setUTCDate(result[1]);
                    continue;
                }
            }

            /* 2.3. If the found-month flag is not set and the date-token matches the
             * month production, set the found-month flag and set the month-value to
             * the month denoted by the date-token.  Skip the remaining sub-steps and
             * continue to the next date-token.
             */
            if (!found_month) {
                result = MONTH.exec(token);
                if (result) {
                    found_month = true;
                    date.setUTCMonth(MONTH_TO_NUM[result[1].toLowerCase()]);
                    continue;
                }
            }

            /* 2.4. If the found-year flag is not set and the date-token matches the year
             * production, set the found-year flag and set the year-value to the number
             * denoted by the date-token.  Skip the remaining sub-steps and continue to
             * the next date-token.
             */
            if (!found_year) {
                result = YEAR.exec(token);
                if (result) {
                    var year = result[0];
                    /* From S5.1.1:
                     * 3.  If the year-value is greater than or equal to 70 and less
                     * than or equal to 99, increment the year-value by 1900.
                     * 4.  If the year-value is greater than or equal to 0 and less
                     * than or equal to 69, increment the year-value by 2000.
                     */
                    if (70 <= year && year <= 99) {
                        year += 1900;
                    } else if (0 <= year && year <= 69) {
                        year += 2000;
                    }

                    if (year < 1601) {
                        return; // 5. ... the year-value is less than 1601
                    }

                    found_year = true;
                    date.setUTCFullYear(year);
                    continue;
                }
            }
        }

        if (!(found_time && found_dom && found_month && found_year)) {
            return; // 5. ... at least one of the found-day-of-month, found-month, found-
                    // year, or found-time flags is not set,
        }

        return date;
    }

    function formatDate(date) {
        var d = date.getUTCDate();
        d = d >= 10 ? d : '0' + d;
        var h = date.getUTCHours();
        h = h >= 10 ? h : '0' + h;
        var m = date.getUTCMinutes();
        m = m >= 10 ? m : '0' + m;
        var s = date.getUTCSeconds();
        s = s >= 10 ? s : '0' + s;
        return NUM_TO_DAY[date.getUTCDay()] + ', ' +
            d + ' ' + NUM_TO_MONTH[date.getUTCMonth()] + ' ' + date.getUTCFullYear() + ' ' +
            h + ':' + m + ':' + s + ' GMT';
    }

// S5.1.2 Canonicalized Host Names
    function canonicalDomain(str) {
        if (str == null) {
            return null;
        }
        str = str.trim().replace(/^\./, ''); // S4.1.2.3 & S5.2.3: ignore leading .


        return str.toLowerCase();
    }

// S5.1.3 Domain Matching
    function domainMatch(str, domStr, canonicalize) {
        if (str == null || domStr == null) {
            return null;
        }
        if (canonicalize !== false) {
            str = canonicalDomain(str);
            domStr = canonicalDomain(domStr);
        }

        /*
         * "The domain string and the string are identical. (Note that both the
         * domain string and the string will have been canonicalized to lower case at
         * this point)"
         */
        if (str == domStr) {
            return true;
        }

        /* "All of the following [three] conditions hold:" (order adjusted from the RFC) */

        /* "* The string is a host name (i.e., not an IP address)." */


        /* "* The domain string is a suffix of the string" */
        var idx = str.indexOf(domStr);
        if (idx <= 0) {
            return false; // it's a non-match (-1) or prefix (0)
        }

        // e.g "a.b.c".indexOf("b.c") === 2
        // 5 === 3+2
        if (str.length !== domStr.length + idx) { // it's not a suffix
            return false;
        }

        /* "* The last character of the string that is not included in the domain
         * string is a %x2E (".") character." */
        if (str.substr(idx - 1, 1) !== '.') {
            return false;
        }

        return true;
    }


// RFC6265 S5.1.4 Paths and Path-Match

    /*
     * "The user agent MUST use an algorithm equivalent to the following algorithm
     * to compute the default-path of a cookie:"
     *
     * Assumption: the path (and not query part or absolute uri) is passed in.
     */
    function defaultPath(path) {
        // "2. If the uri-path is empty or if the first character of the uri-path is not
        // a %x2F ("/") character, output %x2F ("/") and skip the remaining steps.
        if (!path || path.substr(0, 1) !== "/") {
            return "/";
        }

        // "3. If the uri-path contains no more than one %x2F ("/") character, output
        // %x2F ("/") and skip the remaining step."
        if (path === "/") {
            return path;
        }

        var rightSlash = path.lastIndexOf("/");
        if (rightSlash === 0) {
            return "/";
        }

        // "4. Output the characters of the uri-path from the first character up to,
        // but not including, the right-most %x2F ("/")."
        return path.slice(0, rightSlash);
    }

    /*
     * "A request-path path-matches a given cookie-path if at least one of the
     * following conditions holds:"
     */
    function pathMatch(reqPath, cookiePath) {
        // "o  The cookie-path and the request-path are identical."
        if (cookiePath === reqPath) {
            return true;
        }

        var idx = reqPath.indexOf(cookiePath);
        if (idx === 0) {
            // "o  The cookie-path is a prefix of the request-path, and the last
            // character of the cookie-path is %x2F ("/")."
            if (cookiePath.substr(-1) === "/") {
                return true;
            }

            // " o  The cookie-path is a prefix of the request-path, and the first
            // character of the request-path that is not included in the cookie- path
            // is a %x2F ("/") character."
            if (reqPath.substr(cookiePath.length, 1) === "/") {
                return true;
            }
        }

        return false;
    }

    function parse(str, strict) {
        str = str.trim();

        // S4.1.1 Trailing semi-colons are not part of the specification.
        // If we are not in strict mode we remove the trailing semi-colons.
        var semiColonCheck = TRAILING_SEMICOLON.exec(str);
        if (semiColonCheck) {
            if (strict) {
                return;
            }
            str = str.slice(0, semiColonCheck.index);
        }

        // We use a regex to parse the "name-value-pair" part of S5.2
        var firstSemi = str.indexOf(';'); // S5.2 step 1
        var pairRx = strict ? COOKIE_PAIR_STRICT : COOKIE_PAIR;
        var result = pairRx.exec(firstSemi === -1 ? str : str.substr(0, firstSemi));

        // Rx satisfies the "the name string is empty" and "lacks a %x3D ("=")"
        // constraints as well as trimming any whitespace.
        if (!result) {
            return;
        }

        var c = new Cookie();
        c.key = result[1]; // the regexp should trim() already
        c.value = result[2]; // FIXED

        if (firstSemi === -1) {
            return c;
        }

        // S5.2.3 "unparsed-attributes consist of the remainder of the set-cookie-string
        // (including the %x3B (";") in question)." plus later on in the same section
        // "discard the first ";" and trim".
        var unparsed = str.slice(firstSemi).replace(/^\s*;\s*/, '').trim();

        // "If the unparsed-attributes string is empty, skip the rest of these
        // steps."
        if (unparsed.length === 0) {
            return c;
        }

        /*
         * S5.2 says that when looping over the items "[p]rocess the attribute-name
         * and attribute-value according to the requirements in the following
         * subsections" for every item.  Plus, for many of the individual attributes
         * in S5.3 it says to use the "attribute-value of the last attribute in the
         * cookie-attribute-list".  Therefore, in this implementation, we overwrite
         * the previous value.
         */
        var cookie_avs = unparsed.split(/\s*;\s*/);
        while (cookie_avs.length) {
            var av = cookie_avs.shift();

            if (strict && !EXTENSION_AV.test(av)) {
                return;
            }

            var av_sep = av.indexOf('=');
            var av_key, av_value;
            if (av_sep === -1) {
                av_key = av;
                av_value = null;
            } else {
                av_key = av.substr(0, av_sep);
                av_value = av.substr(av_sep + 1);
            }

            av_key = av_key.trim().toLowerCase();
            if (av_value) {
                av_value = av_value.trim();
            }

            switch (av_key) {
                case 'expires': // S5.2.1
                    if (!av_value) {
                        if (strict) {
                            return;
                        } else {
                            break;
                        }
                    }
                    var exp = parseDate(av_value, strict);
                    // "If the attribute-value failed to parse as a cookie date, ignore the
                    // cookie-av."
                    if (exp == null) {
                        if (strict) {
                            return;
                        } else {
                            break;
                        }
                    }
                    c.expires = exp;
                    // over and underflow not realistically a concern: V8's getTime() seems to
                    // store something larger than a 32-bit time_t (even with 32-bit node)
                    break;

                case 'max-age': // S5.2.2
                    if (!av_value) {
                        if (strict) {
                            return;
                        } else {
                            break;
                        }
                    }
                    // "If the first character of the attribute-value is not a DIGIT or a "-"
                    // character ...[or]... If the remainder of attribute-value contains a
                    // non-DIGIT character, ignore the cookie-av."
                    if (!/^-?[0-9]+$/.test(av_value)) {
                        if (strict) {
                            return;
                        } else {
                            break;
                        }
                    }
                    var delta = parseInt(av_value, 10);
                    if (strict && delta <= 0) {
                        return; // S4.1.1
                    }
                    // "If delta-seconds is less than or equal to zero (0), let expiry-time
                    // be the earliest representable date and time."
                    c.setMaxAge(delta);
                    break;

                case 'domain': // S5.2.3
                    // "If the attribute-value is empty, the behavior is undefined.  However,
                    // the user agent SHOULD ignore the cookie-av entirely."
                    if (!av_value) {
                        if (strict) {
                            return;
                        } else {
                            break;
                        }
                    }
                    // S5.2.3 "Let cookie-domain be the attribute-value without the leading %x2E
                    // (".") character."
                    var domain = av_value.trim().replace(/^\./, '');
                    if (!domain) {
                        if (strict) {
                            return;
                        } else {
                            break;
                        }
                    } // see "is empty" above
                    // "Convert the cookie-domain to lower case."
                    c.domain = domain.toLowerCase();
                    break;

                case 'path': // S5.2.4
                    /*
                     * "If the attribute-value is empty or if the first character of the
                     * attribute-value is not %x2F ("/"):
                     *   Let cookie-path be the default-path.
                     * Otherwise:
                     *   Let cookie-path be the attribute-value."
                     *
                     * We'll represent the default-path as null since it depends on the
                     * context of the parsing.
                     */
                    if (!av_value || av_value.substr(0, 1) != "/") {
                        if (strict) {
                            return;
                        } else {
                            break;
                        }
                    }
                    c.path = av_value;
                    break;

                case 'secure': // S5.2.5
                    /*
                     * "If the attribute-name case-insensitively matches the string "Secure",
                     * the user agent MUST append an attribute to the cookie-attribute-list
                     * with an attribute-name of Secure and an empty attribute-value."
                     */
                    if (av_value != null) {
                        if (strict) {
                            return;
                        }
                    }
                    c.secure = true;
                    break;

                case 'httponly': // S5.2.6 -- effectively the same as 'secure'
                    if (av_value != null) {
                        if (strict) {
                            return;
                        }
                    }
                    c.httpOnly = true;
                    break;

                default:
                    c.extensions = c.extensions || [];
                    c.extensions.push(av);
                    break;
            }
        }

        // ensure a default date for sorting:
        c.creation = new Date();
        return c;
    }

    function fromJSON(str) {
        if (!str) {
            return null;
        }

        var obj;
        try {
            obj = JSON.parse(str);
        } catch (e) {
            return null;
        }

        var c = new Cookie();
        for (var i = 0; i < numCookieProperties; i++) {
            var prop = cookieProperties[i];
            if (obj[prop] == null) {
                continue;
            }
            if (prop === 'expires' ||
                prop === 'creation' ||
                prop === 'lastAccessed') {
                c[prop] = obj[prop] == "Infinity" ? "Infinity" : new Date(obj[prop]);
            } else {
                c[prop] = obj[prop];
            }
        }


        // ensure a default date for sorting:
        c.creation = c.creation || new Date();

        return c;
    }

    /* Section 5.4 part 2:
     * "*  Cookies with longer paths are listed before cookies with
     *     shorter paths.
     *
     *  *  Among cookies that have equal-length path fields, cookies with
     *     earlier creation-times are listed before cookies with later
     *     creation-times."
     */

    function cookieCompare(a, b) {
        // descending for length: b CMP a
        var deltaLen = (b.path ? b.path.length : 0) - (a.path ? a.path.length : 0);
        if (deltaLen !== 0) {
            return deltaLen;
        }
        // ascending for time: a CMP b
        return (a.creation ? a.creation.getTime() : MAX_TIME) -
            (b.creation ? b.creation.getTime() : MAX_TIME);
    }

// Gives the permutation of all possible domainMatch()es of a given domain. The
// array is in shortest-to-longest order.  Handy for indexing.
    function permuteDomain(domain) {
        var pubSuf = getPublicSuffix(domain);
        if (!pubSuf) {
            return null;
        }
        if (pubSuf == domain) {
            return [domain];
        }

        var prefix = domain.slice(0, -(pubSuf.length + 1)); // ".example.com"
        var parts = prefix.split('.').reverse();
        var cur = pubSuf;
        var permutations = [cur];
        while (parts.length) {
            cur = parts.shift() + '.' + cur;
            permutations.push(cur);
        }
        return permutations;
    }

// Gives the permutation of all possible pathMatch()es of a given path. The
// array is in longest-to-shortest order.  Handy for indexing.
    function permutePath(path) {
        if (path === '/') {
            return ['/'];
        }
        if (path.lastIndexOf('/') === path.length - 1) {
            path = path.substr(0, path.length - 1);
        }
        var permutations = [path];
        while (path.length > 1) {
            var lindex = path.lastIndexOf('/');
            if (lindex === 0) {
                break;
            }
            path = path.substr(0, lindex);
            permutations.push(path);
        }
        permutations.push('/');
        return permutations;
    }


    function Cookie(opts) {
        if (typeof opts !== "object") {
            return;
        }
        Object.keys(opts).forEach(function (key) {
            if (Cookie.prototype.hasOwnProperty(key)) {
                this[key] = opts[key] || Cookie.prototype[key];
            }
        }.bind(this));
    }

    Cookie.parse = parse;
    Cookie.fromJSON = fromJSON;

    Cookie.prototype.key = "";
    Cookie.prototype.value = "";
    Cookie.prototype.firstAccessedFrom = null;

// the order in which the RFC has them:
    Cookie.prototype.expires = "Infinity"; // coerces to literal Infinity
    Cookie.prototype.maxAge = null; // takes precedence over expires for TTL
    Cookie.prototype.domain = null;
    Cookie.prototype.path = null;
    Cookie.prototype.secure = false;
    Cookie.prototype.httpOnly = false;
    Cookie.prototype.extensions = null;

// set by the CookieJar:
    Cookie.prototype.hostOnly = null; // boolean when set
    Cookie.prototype.pathIsDefault = null; // boolean when set
    Cookie.prototype.creation = null; // Date when set; defaulted by Cookie.parse
    Cookie.prototype.lastAccessed = null; // Date when set
    cookieJar.Cookie = Cookie;

    var cookieProperties = Object.freeze(Object.keys(Cookie.prototype).map(function (p) {
        if (p instanceof Function) {
            return;
        }
        return p;
    }));
    var numCookieProperties = cookieProperties.length;

    Cookie.prototype.inspect = function inspect() {
        var now = Date.now();
        return 'Cookie="' + this.toString() +
            '; hostOnly=' + (this.hostOnly != null ? this.hostOnly : '?') +
            '; aAge=' + (this.lastAccessed ? (now - this.lastAccessed.getTime()) + 'ms' : '?') +
            '; cAge=' + (this.creation ? (now - this.creation.getTime()) + 'ms' : '?') +
            '"';
    };

    Cookie.prototype.validate = function validate() {
        if (!COOKIE_OCTETS.test(this.value)) {
            return false;
        }
        if (this.expires != Infinity && !(this.expires instanceof Date) && !parseDate(this.expires, true)) {
            return false;
        }
        if (this.maxAge != null && this.maxAge <= 0) {
            return false; // "Max-Age=" non-zero-digit *DIGIT
        }
        if (this.path != null && !PATH_VALUE.test(this.path)) {
            return false;
        }

        var cdomain = this.cdomain();
        if (cdomain) {
            if (cdomain.match(/\.$/)) {
                return false; // S4.1.2.3 suggests that this is bad. domainMatch() tests confirm this
            }
            var suffix = getPublicSuffix(cdomain);
            if (suffix == null) { // it's a public suffix
                return false;
            }
        }
        return true;
    };

    Cookie.prototype.setExpires = function setExpires(exp) {
        if (exp instanceof Date) {
            this.expires = exp;
        } else {
            this.expires = parseDate(exp) || "Infinity";
        }
    };

    Cookie.prototype.setMaxAge = function setMaxAge(age) {
        if (age === Infinity || age === -Infinity) {
            this.maxAge = age.toString(); // so JSON.stringify() works
        } else {
            this.maxAge = age;
        }
    };

// gives Cookie header format
    Cookie.prototype.cookieString = function cookieString() {
        var val = this.value;
        if (val == null) {
            val = '';
        }
        return this.key + '=' + val;
    };

// gives Set-Cookie header format
    Cookie.prototype.toString = function toString() {
        var str = this.cookieString();

        if (this.expires != Infinity) {
            if (this.expires instanceof Date) {
                str += '; Expires=' + formatDate(this.expires);
            } else {
                str += '; Expires=' + this.expires;
            }
        }

        if (this.maxAge != null && this.maxAge != Infinity) {
            str += '; Max-Age=' + this.maxAge;
        }

        if (this.domain && !this.hostOnly) {
            str += '; Domain=' + this.domain;
        }
        if (this.path) {
            str += '; Path=' + this.path;
        }

        if (this.secure) {
            str += '; Secure';
        }
        if (this.httpOnly) {
            str += '; HttpOnly';
        }
        if (this.extensions) {
            this.extensions.forEach(function (ext) {
                str += '; ' + ext;
            });
        }

        return str;
    };

// TTL() partially replaces the "expiry-time" parts of S5.3 step 3 (setCookie()
// elsewhere)
// S5.3 says to give the "latest representable date" for which we use Infinity
// For "expired" we use 0
    Cookie.prototype.TTL = function TTL(now) {
        /* RFC6265 S4.1.2.2 If a cookie has both the Max-Age and the Expires
         * attribute, the Max-Age attribute has precedence and controls the
         * expiration date of the cookie.
         * (Concurs with S5.3 step 3)
         */
        if (this.maxAge != null) {
            return this.maxAge <= 0 ? 0 : this.maxAge * 1000;
        }

        var expires = this.expires;
        if (expires != Infinity) {
            if (!(expires instanceof Date)) {
                expires = parseDate(expires) || Infinity;
            }

            if (expires == Infinity) {
                return Infinity;
            }

            return expires.getTime() - (now || Date.now());
        }

        return Infinity;
    };

// expiryTime() replaces the "expiry-time" parts of S5.3 step 3 (setCookie()
// elsewhere)
    Cookie.prototype.expiryTime = function expiryTime(now) {
        if (this.maxAge != null) {
            var relativeTo = this.creation || now || new Date();
            var age = (this.maxAge <= 0) ? -Infinity : this.maxAge * 1000;
            return relativeTo.getTime() + age;
        }

        if (this.expires == Infinity) {
            return Infinity;
        }
        return this.expires.getTime();
    };

// expiryDate() replaces the "expiry-time" parts of S5.3 step 3 (setCookie()
// elsewhere), except it returns a Date
    Cookie.prototype.expiryDate = function expiryDate(now) {
        var millisec = this.expiryTime(now);
        if (millisec == Infinity) {
            return new Date(MAX_TIME);
        } else if (millisec == -Infinity) {
            return new Date(MIN_TIME);
        } else {
            return new Date(millisec);
        }
    };

// This replaces the "persistent-flag" parts of S5.3 step 3
    Cookie.prototype.isPersistent = function isPersistent() {
        return (this.maxAge != null || this.expires != Infinity);
    };

// Mostly S5.1.2 and S5.2.3:
    Cookie.prototype.cdomain =
        Cookie.prototype.canonicalizedDomain = function canonicalizedDomain() {
            if (this.domain == null) {
                return null;
            }
            return canonicalDomain(this.domain);
        };


    var memstore;

    function CookieJar(rejectPublicSuffixes, store) {
        if (rejectPublicSuffixes != null) {
            this.rejectPublicSuffixes = rejectPublicSuffixes;
        }

            if (store) {
                    this.store = store;
            } else {
                    this.store = new MemoryCookieStore(firebaseEndpoint);
            }
    }

    cookieJar.CookieJar = CookieJar;

    CookieJar.prototype.store = null;
    CookieJar.prototype.rejectPublicSuffixes = true;

    CookieJar.prototype.setCookie = function (cookie, url, options, cb) {
        var err;
        var context = (url instanceof Object) ? url : urlParse(url);
        if (options instanceof Function) {
            cb = options;
            options = {};
        }

        var host = canonicalDomain(context.hostname);

        // S5.3 step 1
        if (!(cookie instanceof Cookie)) {
            cookie = Cookie.parse(cookie, options.strict === true);
        }
        if (!cookie) {
            err = new Error("Cookie failed to parse");
            return cb(options.ignoreError ? null : err);
        }

        // S5.3 step 2
        var now = options.now || new Date(); // will assign later to save effort in the face of errors

        // S5.3 step 3: NOOP; persistent-flag and expiry-time is handled by getCookie()

        // S5.3 step 4: NOOP; domain is null by default

        // S5.3 step 5: public suffixes
        if (this.rejectPublicSuffixes && cookie.domain) {
            var suffix = getPublicSuffix(cookie.cdomain());
            if (suffix == null) { // e.g. "com"
                err = new Error("Cookie has domain set to a public suffix");
                return cb(options.ignoreError ? null : err);
            }
        }

        // S5.3 step 6:
        if (cookie.domain) {
            if (!domainMatch(host, cookie.cdomain(), false)) {
                err = new Error("Cookie not in this host's domain. Cookie:" + cookie.cdomain() + " Request:" + host);
                return cb(options.ignoreError ? null : err);
            }

            if (cookie.hostOnly == null) { // don't reset if already set
                cookie.hostOnly = false;
            }

        } else {
            cookie.hostOnly = true;
            cookie.domain = host;
        }

        // S5.3 step 7: "Otherwise, set the cookie's path to the default-path of the
        // request-uri"
        if (!cookie.path) {
            cookie.path = defaultPath(context.pathname);
            cookie.pathIsDefault = true;
        } else {
            if (cookie.path.length > 1 && cookie.path.substr(-1) == '/') {
                cookie.path = cookie.path.slice(0, -1);
            }
        }

        // S5.3 step 8: NOOP; secure attribute
        // S5.3 step 9: NOOP; httpOnly attribute

        // S5.3 step 10
        if (options.http === false && cookie.httpOnly) {
            err = new Error("Cookie is HttpOnly and this isn't an HTTP API");
            return cb(options.ignoreError ? null : err);
        }

        var store = this.store;

        if (!store.updateCookie) {
            store.updateCookie = function (oldCookie, newCookie, cb) {
                this.putCookie(newCookie, cb);
            };
        }

        function withCookie(err, oldCookie) {
            if (err) {
                return cb(err);
            }

            var next = function (err) {
                if (err) {
                    return cb(err);
                } else {
                    cb(null, cookie);
                }
            };

            if (oldCookie) {
                // S5.3 step 11 - "If the cookie store contains a cookie with the same name,
                // domain, and path as the newly created cookie:"
                if (options.http === false && oldCookie.httpOnly) { // step 11.2
                    err = new Error("old Cookie is HttpOnly and this isn't an HTTP API");
                    return cb(options.ignoreError ? null : err);
                }
                cookie.creation = oldCookie.creation; // step 11.3
                cookie.lastAccessed = now;
                // Step 11.4 (delete cookie) is implied by just setting the new one:
                store.updateCookie(oldCookie, cookie, next); // step 12

            } else {
                cookie.creation = cookie.lastAccessed = now;
                store.putCookie(cookie, next); // step 12
            }
        }

        store.findCookie(cookie.domain, cookie.path, cookie.key, withCookie);
    };

    var firstAccessUrls = {};

// RFC6365 S5.4
    CookieJar.prototype.getCookies = function (url, options, persistUrl) {
        var context = (url instanceof Object) ? url : urlParse(url);
        if (!options) {
            options = {};
        }

        var host = canonicalDomain(context.hostname);
        var path = context.pathname || '/';

        var secure = options.secure;
        if (secure == null && context.protocol &&
            (context.protocol == 'https:' || context.protocol == 'wss:')) {
            secure = true;
        }

        var http = options.http;
        if (http == null) {
            http = true;
        }

        var now = options.now || Date.now();
        var expireCheck = options.expire !== false;
        var allPaths = !!options.allPaths;
        var store = this.store;

        function matchingCookie(c) {
            // "Either:
            //   The cookie's host-only-flag is true and the canonicalized
            //   request-host is identical to the cookie's domain.
            // Or:
            //   The cookie's host-only-flag is false and the canonicalized
            //   request-host domain-matches the cookie's domain."
            if (c.hostOnly) {
                if (c.domain != host) {
                    return false;
                }
            } else {
                if (!domainMatch(host, c.domain, false)) {
                    return false;
                }
            }

            // "The request-uri's path path-matches the cookie's path."
            if (!allPaths && !pathMatch(path, c.path)) {
                return false;
            }

            // "If the cookie's secure-only-flag is true, then the request-uri's
            // scheme must denote a "secure" protocol"
            if (c.secure && !secure) {
                return false;
            }

            // "If the cookie's http-only-flag is true, then exclude the cookie if the
            // cookie-string is being generated for a "non-HTTP" API"
            if (c.httpOnly && !http) {
                return false;
            }

            // deferred from S5.3
            // non-RFC: allow retention of expired cookies by choice
            if (expireCheck && c.expiryTime() <= now) {
                store.removeCookie(c.domain, c.path, c.key, function () {
                }); // result ignored
                return false;
            }

            return true;
        }

        var cookies = store.findCookies(host, allPaths ? null : path);

        cookies = cookies.filter(matchingCookie);

        // sorting of S5.4 part 2
        if (options.sort !== false) {
            cookies = cookies.sort(cookieCompare);
        }

        // S5.4 part 3
        var now = new Date();
        cookies.forEach(function (c) {
            c.lastAccessed = now;
            var cookieKey = btoa(c.domain + ';' + c.path + ';' + c.key);
            if (persistUrl && !firstAccessUrls[cookieKey]) {
                firstAccessUrls[cookieKey] = url;
            }
        });
        // TODO persist lastAccessed

        return cookies;
    };

    CookieJar.prototype.getCookieString = function (/*..., cb*/) {
        var args = Array.prototype.slice.call(arguments, 0);
        var cb = args.pop();
        var next = function (err, cookies) {
            if (err) {
                cb(err);
            } else {
                cb(null, cookies.map(function (c) {
                    return c.cookieString();
                }).join('; '));
            }
        };
        args.push(next);
        this.getCookies.apply(this, args);
    };

    CookieJar.prototype.getSetCookieStrings = function (/*..., cb*/) {
        var args = Array.prototype.slice.call(arguments, 0);
        var cb = args.pop();
        var next = function (err, cookies) {
            if (err) {
                cb(err);
            } else {
                cb(null, cookies.map(function (c) {
                    return c.toString();
                }));
            }
        };
        args.push(next);
        this.getCookies.apply(this, args);
    };
}());